🎖️GitЯра🎖️
Node / meshtastic / Meshtastic-Android / files / core / ble / src / commonTest / kotlin / org / meshtastic / core / ble / KableMeshtasticRadioProfileExceptionTest.kt
Displaying Raw • Download
core/ble/src/commonTest/kotlin/org/meshtastic/core/ble/KableMeshtasticRadioProfileExceptionTest.kt 4d4070c8e1ed73a56ee2f054300a0f62e3fe2e62 (4d4070c8) Text, 7.04 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.ble
Tff7b72import T7ee787com.juul.kable.GattStatusException
Tff7b72import T7ee787com.juul.kable.NotConnectedException
Tff7b72import T7ee787kotlinx.coroutines.CancellationException
Tff7b72import T7ee787kotlinx.coroutines.ExperimentalCoroutinesApi
Tff7b72import T7ee787kotlinx.coroutines.flow.collect
Tff7b72import T7ee787kotlinx.coroutines.flow.first
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.test.advanceTimeBy
Tff7b72import T7ee787kotlinx.coroutines.test.advanceUntilIdle
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787org.meshtastic.core.testing.FakeBleService
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertFailsWith
Tff7b72import T7ee787kotlin.test.assertTrue
T8b949e/**
* Tests that [KableMeshtasticRadioProfile.fromRadio] propagates session-fatal BLE exceptions (so the transport layer
* can detect broken sessions) while suppressing transient errors.
*/
Tf0883e@OptInTb4b4b4(Te6edf3ExperimentalCoroutinesApiTff7b72::Te6edf3classTb4b4b4)
Tff7b72class T56d364KableMeshtasticRadioProfileExceptionTest Tb4b4b4{
Tff7b72private Tff7b72fun Td2a8ffcreateServiceTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3FakeBleService Tff7b72= Te6edf3FakeBleServiceTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3addCharacteristicTb4b4b4(Te6edf3MeshtasticBleConstantsTb4b4b4.Te6edf3FROMNUM_CHARACTERISTICTb4b4b4)
Te6edf3addCharacteristicTb4b4b4(Te6edf3MeshtasticBleConstantsTb4b4b4.Te6edf3FROMRADIO_CHARACTERISTICTb4b4b4)
Te6edf3addCharacteristicTb4b4b4(Te6edf3MeshtasticBleConstantsTb4b4b4.Te6edf3TORADIO_CHARACTERISTICTb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`fromRadio propagates NotConnectedException to collector`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3service Tff7b72= Te6edf3createServiceTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3profile Tff7b72= Te6edf3KableMeshtasticRadioProfileTb4b4b4(Te6edf3serviceTb4b4b4)
T8b949e// Set up the read to throw NotConnectedException immediately
Te6edf3serviceTb4b4b4.Te6edf3readException Tff7b72= Te6edf3NotConnectedExceptionTb4b4b4(Ta5d6ff"Ta5d6ffsession closedTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3assertFailsWithTff7b72<Te6edf3NotConnectedExceptionTff7b72> Tb4b4b4{ Te6edf3profileTb4b4b4.Te6edf3fromRadioTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3messageTff7b72!!Tb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffsession closedTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`fromRadio propagates fatal GattStatusException to collector`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3service Tff7b72= Te6edf3createServiceTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3profile Tff7b72= Te6edf3KableMeshtasticRadioProfileTb4b4b4(Te6edf3serviceTb4b4b4)
T8b949e// Fatal GATT status (133 = GATT_ERROR, in FATAL_GATT_STATUSES)
Te6edf3serviceTb4b4b4.Te6edf3readException Tff7b72= Te6edf3GattStatusExceptionTb4b4b4(Te6edf3message Tff7b72= Ta5d6ff"Ta5d6ffGATT errorTa5d6ff"Tb4b4b4, Te6edf3status Tff7b72= T79c0ff1T79c0ff3T79c0ff3Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3assertFailsWithTff7b72<Te6edf3GattStatusExceptionTff7b72> Tb4b4b4{ Te6edf3profileTb4b4b4.Te6edf3fromRadioTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3messageTff7b72!!Tb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffGATT errorTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`fromRadio propagates CancellationException`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3service Tff7b72= Te6edf3createServiceTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3profile Tff7b72= Te6edf3KableMeshtasticRadioProfileTb4b4b4(Te6edf3serviceTb4b4b4)
T8b949e// Set up the read to throw CancellationException
Te6edf3serviceTb4b4b4.Te6edf3readException Tff7b72= Te6edf3CancellationExceptionTb4b4b4(Ta5d6ff"Ta5d6ffcancelledTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3result Tff7b72= Te6edf3assertFailsWithTff7b72<Te6edf3CancellationExceptionTff7b72> Tb4b4b4{ Te6edf3profileTb4b4b4.Te6edf3fromRadioTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3messageTff7b72!!Tb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffcancelledTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`fromRadio suppresses transient GattStatusException and continues`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3service Tff7b72= Te6edf3createServiceTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3profile Tff7b72= Te6edf3KableMeshtasticRadioProfileTb4b4b4(Te6edf3serviceTb4b4b4)
T8b949e// Non-fatal GATT status (e.g. status 6 = GATT_BUSY, not in FATAL_GATT_STATUSES)
Te6edf3serviceTb4b4b4.Te6edf3readException Tff7b72= Te6edf3GattStatusExceptionTb4b4b4(Te6edf3message Tff7b72= Ta5d6ff"Ta5d6fftransient busyTa5d6ff"Tb4b4b4, Te6edf3status Tff7b72= T79c0ff6Tb4b4b4)
T8b949e// Collect from the flow in a coroutine; after the transient error is suppressed and the
T8b949e// delay elapses, the next drain trigger should restart reading. Enqueue a real packet so
T8b949e// the collector eventually receives something.
Tff7b72var Te6edf3collected Tff7b72= Tff7b72false
Tff7b72val Te6edf3collectJob Tff7b72= Te6edf3launch Tb4b4b4{ Te6edf3profileTb4b4b4.Te6edf3fromRadioTb4b4b4.Te6edf3collect Tb4b4b4{ Te6edf3collected Tff7b72= Tff7b72true Tb4b4b4} Tb4b4b4}
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
T8b949e// The transient error was suppressed (500ms delay). Advance past it.
Te6edf3advanceTimeByTb4b4b4(T79c0ff6T79c0ff0T79c0ff0Tb4b4b4)
T8b949e// Now enqueue a packet so the next drain cycle emits data, proving the flow survived.
Te6edf3serviceTb4b4b4.Te6edf3enqueueReadTb4b4b4(Te6edf3MeshtasticBleConstantsTb4b4b4.Te6edf3FROMRADIO_CHARACTERISTICTb4b4b4, Te6edf3byteArrayOfTb4b4b4(T79c0ff4T79c0ff2Tb4b4b4)Tb4b4b4)
Te6edf3serviceTb4b4b4.Te6edf3enqueueReadTb4b4b4(Te6edf3MeshtasticBleConstantsTb4b4b4.Te6edf3FROMRADIO_CHARACTERISTICTb4b4b4, Te6edf3ByteArrayTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4)
Te6edf3profileTb4b4b4.Te6edf3requestDrainTb4b4b4(Tb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3collectedTb4b4b4, Ta5d6ff"Ta5d6ffFlow should have emitted a packet after transient error was suppressedTa5d6ff"Tb4b4b4)
Te6edf3collectJobTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Tb4b4b4}
T8b949e// --- logRadio fatal exception propagation tests ---
Tf0883e@Test
Tff7b72fun Td2a8ff`logRadio propagates NotConnectedException from observation`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3service Tff7b72= Te6edf3createServiceTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{ Te6edf3addCharacteristicTb4b4b4(Te6edf3MeshtasticBleConstantsTb4b4b4.Te6edf3LOGRADIO_CHARACTERISTICTb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3profile Tff7b72= Te6edf3KableMeshtasticRadioProfileTb4b4b4(Te6edf3serviceTb4b4b4)
Te6edf3serviceTb4b4b4.Te6edf3observeException Tff7b72= Te6edf3NotConnectedExceptionTb4b4b4(Ta5d6ff"Ta5d6fflog radio session closedTa5d6ff"Tb4b4b4)
Te6edf3assertFailsWithTff7b72<Te6edf3NotConnectedExceptionTff7b72> Tb4b4b4{ Te6edf3profileTb4b4b4.Te6edf3logRadioTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}
Tf0883e@Test
Tff7b72fun Td2a8ff`logRadio propagates fatal GattStatusException from observation`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3service Tff7b72= Te6edf3createServiceTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{ Te6edf3addCharacteristicTb4b4b4(Te6edf3MeshtasticBleConstantsTb4b4b4.Te6edf3LOGRADIO_CHARACTERISTICTb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3profile Tff7b72= Te6edf3KableMeshtasticRadioProfileTb4b4b4(Te6edf3serviceTb4b4b4)
Te6edf3serviceTb4b4b4.Te6edf3observeException Tff7b72= Te6edf3GattStatusExceptionTb4b4b4(Te6edf3status Tff7b72= T79c0ff1T79c0ff3T79c0ff3Tb4b4b4, Te6edf3message Tff7b72= Ta5d6ff"Ta5d6ffGATT errorTa5d6ff"Tb4b4b4)
Te6edf3assertFailsWithTff7b72<Te6edf3GattStatusExceptionTff7b72> Tb4b4b4{ Te6edf3profileTb4b4b4.Te6edf3logRadioTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}
T8b949e// --- subscriptionReady exceptional completion tests ---
Tf0883e@Test
Tff7b72fun Td2a8ff`awaitSubscriptionReady throws promptly when FROMNUM observe fails before readiness`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Tff7b72val Te6edf3service Tff7b72= Te6edf3createServiceTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3profile Tff7b72= Te6edf3KableMeshtasticRadioProfileTb4b4b4(Te6edf3serviceTb4b4b4)
T8b949e// Set observeException — when fromRadio is collected, the FROMNUM observe will throw
T8b949e// before subscriptionReady is completed. The fix completes it exceptionally.
Te6edf3serviceTb4b4b4.Te6edf3observeException Tff7b72= Te6edf3NotConnectedExceptionTb4b4b4(Ta5d6ff"Ta5d6ffobserve failed before CCCDTa5d6ff"Tb4b4b4)
T8b949e// Start collecting fromRadio in a regular child coroutine so it's properly scoped
T8b949e// and cancelled by the test framework. The exception is caught inside the coroutine,
T8b949e// so it won't crash the test scope.
Tff7b72val Te6edf3collectJob Tff7b72= Te6edf3launch Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3profileTb4b4b4.Te6edf3fromRadioTb4b4b4.Te6edf3collect Tb4b4b4{Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
T8b949e// Expected — the observe failure propagates through the channelFlow
Tb4b4b4}
Tb4b4b4}
Tff7b72try Tb4b4b4{
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)
T8b949e// awaitSubscriptionReady should throw the exception promptly, not hang
Tff7b72val Te6edf3result Tff7b72= Te6edf3assertFailsWithTff7b72<Te6edf3NotConnectedExceptionTff7b72> Tb4b4b4{ Te6edf3profileTb4b4b4.Te6edf3awaitSubscriptionReadyTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3assertTrueTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3messageTff7b72!!Tb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffobserve failed before CCCDTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4} Tff7b72finally Tb4b4b4{
Te6edf3collectJobTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Served by rngit 1.5.0 - Generated in 0.24s